package com.eliasbalasis.dwr4gwt.client;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.juglar.gwt.jsonizer.client.Jsonizer;
import org.juglar.gwt.jsonizer.client.JsonizerException;
import org.juglar.gwt.jsonizer.client.JsonizerParser;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.user.client.ui.Composite;

public class DwrAdapter extends Composite implements IDwrAdapter {
	private Map remoteMethodCallDescriptors = new HashMap();
	private Vector listeners = new Vector();

	public DwrAdapter() {
		registerErrorHandler();
		registerWarningHandler();
	}

	private native void jsRegisterErrorHandler() /*-{
		$wnd.dwr.engine.setErrorHandler(
			function(errorString, exception) {
				this.@com.eliasbalasis.dwr4gwt.client.DwrAdapter::fireDwrError(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)
				(
				errorString,
				exception
				);
			}
		);
	}-*/;
	private native void jsRegisterWarningHandler() /*-{
		$wnd.dwr.engine.setWarningHandler(
			function(errorString, exception) {
				this.@com.eliasbalasis.dwr4gwt.client.DwrAdapter::fireDwrWarning(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)
				(
				errorString,
				exception
				);
			}
		);
	}-*/;
	private void registerErrorHandler() {
		jsRegisterErrorHandler();
	}
	private void registerWarningHandler() {
		jsRegisterWarningHandler();
	}

	private void fireDwrError(String message, JavaScriptObject exception) {
		DwrEvent event = new DwrEvent(this,null);
		event.setType(DwrEvent.TYPE_GENERAL_ERROR);
		DwrError error = new DwrError();
		error.setMessage(message);
		JSONObject jsonException = new JSONObject(exception);
		error.setException(
			new JavaScriptException(
				jsonException.get("name").isString().stringValue(),
				jsonException.get("message").isString().stringValue()
				)
			);
		Iterator listeners = this.listeners.iterator();
		while(listeners.hasNext()) {
			DwrListener listener = (DwrListener)listeners.next();
			listener.onDwrError(event);
		}
	}

	private void fireDwrWarning(String message, JavaScriptObject exception) {
		DwrEvent event = new DwrEvent(this,null);
		event.setType(DwrEvent.TYPE_GENERAL_WARNING);
		DwrWarning warning = new DwrWarning();
		warning.setMessage(message);
		JSONObject jsonException = new JSONObject(exception);
		warning.setException(
			new JavaScriptException(
				jsonException.get("name").isString().stringValue(),
				jsonException.get("message").isString().stringValue()
				)
			);
		Iterator listeners = this.listeners.iterator();
		while(listeners.hasNext()) {
			DwrListener listener = (DwrListener)listeners.next();
			listener.onDwrWarning(event);
		}
	}

	public void callRemoteMethod(DwrRemoteMethodCallDescriptor remoteMethodCallDescriptor) throws DwrAdapterException {
		String remoteClassName = remoteMethodCallDescriptor.getRemoteClassName();
		if (remoteClassName==null)
			throw new DwrAdapterException("invalid parameter remoteProxyName "+"("+remoteClassName+")");
		if (remoteClassName.trim().length()==0)
			throw new DwrAdapterException("invalid parameter remoteProxyName "+"("+remoteClassName+")");
		String remoteMethodName = remoteMethodCallDescriptor.getRemoteMethodName();
		if (remoteMethodName==null)
			throw new DwrAdapterException("invalid parameter methodName "+"("+remoteMethodName+")");
		if (remoteMethodName.trim().length()==0)
			throw new DwrAdapterException("invalid parameter methodName "+"("+remoteMethodName+")");
		Object parameters = remoteMethodCallDescriptor.getParameterValues();
		Jsonizer parametersJsonizer = remoteMethodCallDescriptor.getParametersMarshaller();
		if (parameters!=null && parametersJsonizer==null) {
			throw new DwrAdapterException("parameters marshaller required");
		}
		JavaScriptObject jsParameters = null;
		String jsonParameters = null;
		if (parameters!=null && parametersJsonizer!=null) {
			jsonParameters = parametersJsonizer.asString(parameters);
			jsParameters = JsonizerParser.evaluate(jsonParameters);
		}
		Integer remoteMethodCallDescriptorKey =
			new Integer(
				remoteMethodCallDescriptor.hashCode()+
				remoteMethodCallDescriptors.size()
				);
		JavaScriptObject jsParameterNames = null;
		DwrRemoteMethodParameterNamesDescriptor parameterNames = new DwrRemoteMethodParameterNamesDescriptor();
		if (remoteMethodCallDescriptor.getParameterNames()!=null) {
			int index = 0;
			for (index = 0;index<remoteMethodCallDescriptor.getParameterNames().length;++index) {
				parameterNames.getNames().add(remoteMethodCallDescriptor.getParameterNames()[index]);
			}
			Jsonizer parameterNamesJsonizer = ((Jsonizer)GWT.create(DwrRemoteMethodParameterNamesDescriptorJsonizer.class));
			jsParameterNames = JsonizerParser.evaluate(parameterNamesJsonizer.asString(parameterNames));
		}
		try {
			jsCallRemoteMethod(
				remoteClassName,
				remoteMethodName,
				jsParameters,
				jsParameterNames,
				remoteMethodCallDescriptorKey.intValue(),
				remoteMethodCallDescriptor.getTimeoutMsec()
				);
		} catch(JsonizerException ex) {
			throw new DwrAdapterException("Failed to call remote DWR method "+remoteClassName+"."+remoteMethodName,ex);
		}
		remoteMethodCallDescriptor.setId(remoteMethodCallDescriptorKey);
		remoteMethodCallDescriptors.put(remoteMethodCallDescriptorKey, remoteMethodCallDescriptor);
	}

	private native void jsCallRemoteMethod(
			String remoteClassName,
			String remoteMethodName,
			JavaScriptObject callParameters,
			JavaScriptObject callParameterNames,
			int remoteMethodCallDescriptorKey,
			int callTimeout) /*-{
		var me = this;
		var remoteClass = eval('$wnd.'+remoteClassName);
		var remoteMethod = remoteClass[remoteMethodName];
		var callMetadata = {
			callback:
				function(returnValue) {
					var returnValueShell = {};
					var isReturnValueOfPrimitiveType = false;
					var isReturnValueOfArrayType = false;
					if (typeof(returnValue)=='object') {
						//$wnd.alert("returnValue is an object");
						returnValueShell = returnValue;
					}
					if (typeof(returnValue)=='number' || typeof(returnValue)=='string' || typeof(returnValue)=='boolean') {
						//$wnd.alert("returnValue is of primitive type (number,string,boolean)");
						returnValueShell = {value: returnValue};
						isReturnValueOfPrimitiveType = true;
					}
					if (returnValue.constructor.toString().indexOf("Array") != -1) {
						//$wnd.alert("returnValue is an array");
						isReturnValueOfArrayType = true;
						returnValueShell = returnValue;
					}
					//$wnd.alert("isReturnValueOfPrimitiveType = "+isReturnValueOfPrimitiveType);
					//$wnd.alert("isReturnValueOfArrayType = "+isReturnValueOfArrayType);
					me.@com.eliasbalasis.dwr4gwt.client.DwrAdapter::fireRemoteMethodCallReply(Lcom/google/gwt/core/client/JavaScriptObject;IZZ)
					(
					returnValueShell,
					remoteMethodCallDescriptorKey,
					isReturnValueOfPrimitiveType,
					isReturnValueOfArrayType
					);
				},
			timeout:
				callTimeout,
			errorHandler:
				function(errorMessage) {
					me.@com.eliasbalasis.dwr4gwt.client.DwrAdapter::fireRemoteMethodCallError(Ljava/lang/String;I)
					(
					errorMessage,
					remoteMethodCallDescriptorKey
					);
				}
			};

			var callParametersArray = new Array();
			var index = 0;
			if (callParameterNames==null) {
				for (var callParameterName in callParameters) {
					callParametersArray[index] = callParameters[callParameterName];
					index += 1;
				}
			} else {
				var callParameterName;
				for (index=0;index<callParameterNames.names.length;++index) {
					callParameterName = callParameterNames.names[index];
					callParametersArray[index] = callParameters[callParameterName];
				}
			}
			callParametersArray[index] = callMetadata;
			remoteMethod.apply(this,callParametersArray);
	}-*/;

	private void fireRemoteMethodCallReply(
			JavaScriptObject jsReturnValue,
			int remoteMethodCallDescriptorKey,
			boolean isReturnValueOfPrimitiveType,
			boolean isReturnValueOfArrayType
			)
			throws DwrAdapterException {
		DwrRemoteMethodCallDescriptor remoteMethodCallDescriptor =
			(DwrRemoteMethodCallDescriptor)
			remoteMethodCallDescriptors.get(new Integer(remoteMethodCallDescriptorKey));
		if (remoteMethodCallDescriptor==null) {
			throw new RuntimeException(new DwrAdapterException("internal error. remoteMethodCallDescriptor not found"));
		}
		remoteMethodCallDescriptors.remove(new Integer(remoteMethodCallDescriptorKey));
		remoteMethodCallDescriptor.setJavaScriptReturnValue(jsReturnValue);
		remoteMethodCallDescriptor.setReturnValueOfPrimitiveType(isReturnValueOfPrimitiveType);
		remoteMethodCallDescriptor.setReturnValueOfArrayType(isReturnValueOfArrayType);
		DwrEvent event = new DwrEvent(this,remoteMethodCallDescriptor);
		event.setType(DwrEvent.TYPE_REMOTECALL_REPLY);
		Iterator listeners = this.listeners.iterator();
		while(listeners.hasNext()) {
			DwrListener listener = (DwrListener)listeners.next();
			listener.onRemoteMethodCallReply(event);
		}
	};

	private void fireRemoteMethodCallError(
			String errorMessage,
			int remoteMethodCallDescriptorKey)
			throws DwrAdapterException {
		DwrRemoteMethodCallDescriptor remoteMethodCallDescriptor =
			(DwrRemoteMethodCallDescriptor)
			remoteMethodCallDescriptors.get(new Integer(remoteMethodCallDescriptorKey));
		if (remoteMethodCallDescriptor==null) {
			throw new RuntimeException(new DwrAdapterException("internal error. remoteMethodCallDescriptor not found"));
		}
		remoteMethodCallDescriptors.remove(new Integer(remoteMethodCallDescriptorKey));
		DwrEvent event = new DwrEvent(this,remoteMethodCallDescriptor);
		event.setType(DwrEvent.TYPE_REMOTECALL_ERROR);
		event.setDwrException(new DwrError(errorMessage));
		Iterator listeners = this.listeners.iterator();
		while(listeners.hasNext()) {
			DwrListener listener = (DwrListener)listeners.next();
			listener.onRemoteMethodCallError(event);
		}
	};

	public void addDwrListener(DwrListener listener) {
		listeners.add(listener);
	}

	public void removeDwrListener(DwrListener listener) {
		listeners.remove(listener);
	}

	public native void jsEnableReverseAJAX(boolean bEnable) /*-{
		$wnd.dwr.engine.setActiveReverseAjax(bEnable);
	}-*/;

	public void EnableReverseAJAX(boolean bEnable) throws DwrAdapterException {
		try {
			jsEnableReverseAJAX(bEnable);
		} catch(Exception ex) {
			throw new DwrAdapterException("Failed to "+(bEnable?"enable":"disable")+" reverse AJAX",ex);
		}
	}
}
